package BNS.plumbing;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.PrintStream;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.Enumeration;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import BNS.graphs.GraphPack;
import BNS.graphs.Node;

/**
 * This class contains general utilities to control some of the behaviour of a
 * BNS game. To fully define the game, an implementation must extend this class
 * and provide at least one method for the BNSPersona to change the game state.
 * (Said methods are all freely designed by the game deviser).
 * <p>
 * The getUpdate method will be called by a ClientLiaison to find out what state
 * the game is in, and will report it in a performance-controlled manner to the
 * game's Persona running in the BNS client. What getUpdate() returns is data
 * specific to the node.
 * </p>
 * <p>
 * Every client will be making independent calls on the getUpdate method, and
 * each one will be represented in a separate Thread, so beware of time, memory,
 * and threading demands. Do not perform long computations that delay or
 * entangle the calling Threads. Avoid synchronizing on anything for more than
 * exceedingly short time spans. A suggested strategy is to use the direct calls
 * from the clients (the ones that cause the game to change state) to perform
 * calculations, since they will be an order of magnitude less frequent than the
 * calls to getUpdate.
 * </p>
 * <p>
 * All methods here are local and cannot be called from anywhere except the JVM
 * that established this object.
 * </p>
 * <p>
 * If -during development- the game is locally available to the implementation
 * of BNSPersona, it may be called directly; consider this is a convenience for
 * the debugging phase. When deployed, however, the game will be remote from the
 * Persona and cannot be called; for reasons involving communication
 * performance, connection monitoring, and latency control, the design prohibits
 * the Persona from obtaining its own updates.
 */
public abstract class BNSGameController implements java.io.Serializable {

  /** the pack of master graph and all its graphlets */
  protected GraphPack grpk;

  /**
   * a flag to indicate whether the game is over. The constructor provided
   * automatically sets this to false, which is one reason to invoke super() at
   * the beginning of your constructor.
   */
  protected boolean gameCompleted;

  /**
   * a String that holds the result of the game (e.g. "won", "lost", "drawn",
   * "failed", "succeeded", etc) used to indicate result to players.
   */
  protected String gameValue;

  /**
   * returns a String that holds the result of the game (e.g. "won", "lost",
   * "drawn", "failed", "succeeded", etc) used to indicate a result to players.
   */
  public String gameValue() {
    return gameValue;
  }

  /** the maximum number of seconds this game will be allowed to run */
  protected int timeLimit; // in seconds (not milliseconds!)

  /** returns the maximum number of seconds this game will be allowed to run */
  public int getTimeLimit() {
    return timeLimit;
  }

  /** the Manager this GameController is using */
  protected Manager manager;

  /** the clock this GameController is using */
  protected Timing clock;

  /** a flag indicating if the game has been loaded yet. */
  protected boolean loaded;

  /** the Monitor object being used to display information to the supervisor */
  protected Monitor monitor;

  /** where session data (about the sequence of games) will be written */
  protected PrintStream sessionLog;

  /** where the player actions and other notable game events will be written */
  protected PrintStream gameLog;

  private transient File sourceFile;

  /** accepts the name of a File and stores it for later reference. */
  protected void setSource(File f) {
    sourceFile = f;
    System.err.println("setSource " + f);
  }

  /** returns the name of a File that was earlier stored by setSource(). */
  protected File getSource() {
    System.err.println("getSource " + sourceFile);
    return sourceFile;
  }

  /**
   * Constructs a generic GameController with a Monitor. Since the games are
   * serialized and stored for later use, constructors that extend this class
   * should acquire all necessary information but not start expanding that data
   * into any larger datastructures. Specifically, it should not create any GUI
   * elements; those are to be built in the init() method, which is called just
   * before the time of actual game play.
   */
  protected BNSGameController(int time) {
    timeLimit = time;
    gameCompleted = false;
  }

  /**
   * a routine that gets called at run time, to finish setting up things that
   * cannot be initialized in the constructor (viz. the constructor should not
   * create any GUI elements.)
   * 
   * @param mgr
   *          the Session manager, which can be queried for information about
   *          the clients, players, etc. that are assigned to this game.
   * @param locn
   *          the pixel location of where the top left corner of the monitor GUI
   *          should be placed.
   * @param sessionLog
   *          the stream where metadata should be recorded.
   * @param gameLog
   *          the stream where game event data should be recorded.
   */
  public void init(Manager mgr, Point locn, PrintStream sessionLog, PrintStream gameLog) {
    this.sessionLog = sessionLog;
    this.gameLog = gameLog;
    manager = mgr;
    this.clock = manager.clock;
    gameValue = "initialized";
    System.err.println("BNSGameController.init()ed");
  }

  /**
   * TODO: only SessionHost calls this; can it be redirected to init() instead?
   * a routine that gets called at run time, to finish setting up things that
   * cannot be initialized in the constructor (viz. the constructor should not
   * create any GUI elements.)
   * 
   * @param supLocn
   *          the pixel location of where the top left corner of the monitor GUI
   *          should be placed.
   * @param mgr
   *          the manager, which can be queried for information about the
   *          clients, players, etc. that are assigned to this game.
   */
  public void installGame(Point supLocn, Manager mgr, PrintStream sessionLog) {
    System.err.println("installGame " + getTitle() + " needs " + getSize() + " players");
    this.gameLog = Library.newGameLog(getSource());
    // debugFrame.main(null);
    // TODO? manager.begin();
    System.err.println("BNSGameController.installGame()ed");
    init(mgr, supLocn, sessionLog, gameLog);
  }

  /** gives a short title for this game; used on window title banner */
  public abstract String getTitle();

  /**
   * gives a long description of this game; used to inform supervisor about game
   * choice.
   */
  public String getInfo() {
    return getTitle() + " of size " + getSize();
  }

  /** reveals how many clients will be needed during this game */
  abstract public int getSize();

  /** provides an enumeration of all nodes active during this game */
  public Enumeration<Node> enumNodes() {
    return grpk.graph.enumNodes();
  }

  private boolean loadGame() {
    // if loading will take a while (involve a human?) then release this thread.
    if (loaded)
      return false; // Presently, only 1 game allowed at a time.
    sessionLog.println("BNSGameController.loadGame");
    gameLog.println("game " + getSource());
    loaded = manager.assignClients(this, grpk.graph.enumNodes());
    if (loaded) {
      gameLog.print("degr");
      Enumeration<Node> e = grpk.graph.enumNodes();
      while (e.hasMoreElements()) {
        Node n = e.nextElement();
        gameLog.print(" " + n.degree());
      }
      gameLog.println();
      manager.begin();
      gameValue = "not started";
    } else
      gameValue = "failed to load";
    return loaded;
  }

  private void endGame() {
    System.err.println("ending " + getTitle());
    manager.closeGame();
    monitor.shutdown();
  }

  /**
   * Controls progress of the game. After this call, actions by the players will
   * be processed until suspend() is called, or until finished() is called.
   */
  public synchronized void proceed() {
    if (gameCompleted) {
      System.err.println("cannot restart completed game");
      return;
    }
    gameValue = "proceeding";
    if (clock.suspended()) {
      clock.proceed();
      sessionLog.println("game proceeding " + System.currentTimeMillis());
    } else
      System.err.println("game still proceeding");
    manager.bulletin(getTitle() + " in progress");
  }

  /**
   * Controls progress of the game. After this call, actions by the players will
   * be ignored, but if proceed() is called subsequently, actions will be
   * re-enabled.
   */
  public synchronized void suspend() {
    gameValue = "suspended";
    if (!clock.suspended()) {
      clock.suspend();
      sessionLog.println("game suspended " + System.currentTimeMillis());
    }
    System.err.println("game suspended");
    manager.bulletin(getTitle() + " suspended");
  }

  /**
   * called by subclass or by the supervisor to terminate the game. It will
   * announce to the supervisor that the game is complete, and the subclass is
   * expected to stop all future operations from clients.
   * 
   * @param message
   *          a string that will be relayed to the supervisor.
   */
  public synchronized void finished(String message, int time) {
    gameCompleted = true;
    suspend();
    System.err.println("message from game Supervisor: " + message);
    sessionLog.println(message + "  " + System.currentTimeMillis());
    Toolkit.getDefaultToolkit().beep();
    gameValue = message;
    showPayoffs(time);
    // gameLog.println("finished "+getSource());
  }

  /**
   * This method should send every client a message saying what hisr final
   * payoff was, write the same thing in the gameLog, and write the total payout
   * in the gameLog. Most games will need to override this method.
   */
  public void showPayoffs(int time) {
  }

  /**
   * calculates the payoff so far for a particular node.
   * 
   * @param nodeID
   *          the index of the node being queried
   * @param time
   *          the game time (in milliseconds) where the game is deemed to end
   * @return the payoff accumulated so far in this game
   */
  public abstract float getPayoff(int nodeID, int time);

  /**
   * returns an arbitrary Object which describes the state of the game from the
   * viewpoint of the given node. This will normally be called by the
   * ClientLiaison and the results will be transported to the client that is
   * operating that node, whereupon they will appear as the argument to
   * BNSPersona.update(). The Persona is responsible for casting the value to
   * the appropriate class type and displaying its information on the GUI.
   * <p>
   * See notes for this class.
   * </p>
   * <p>
   * There is a potential to thwart the purpose of the game if what is returned
   * from this method exposes anything more than what a single player is
   * supposed to be able to know. A knowledgeable hacker could write his own
   * Client program to discover this data and solve the game in an unintended
   * fashion. If the software is used in a very controlled environment, this is
   * not an issue; if it will ever be used in an anonymous mode across the
   * internet, this loophole may need closing.
   * </p>
   * 
   * @param clientID
   *          the identification number of the client.
   * @return a data structure describing the state of the game pertaining to the
   *         node (nodes?) this client is playing.
   */
  public abstract Serializable getUpdate(int clientID);

  /**
   * Constructs and returns a BNSPersona for a given client to play a given
   * node.
   * 
   * @param client
   *          the ClientLiaison that will represent the new Persona on the
   *          server.
   * @param node
   *          the identifying index of the node in the game graph.
   * @return the BNSPersona object that runs in the client to implement the
   *         interface to the player
   */
  public abstract BNSPersona getPersona(ClientLiaison client, Node node);

  /**
   * This method is called by the clients to cause some sort of change to the
   * game state. Every "move" taken by the client causes one call to this
   * method.
   */
  public abstract boolean alter(ClientLiaison client, Serializable changeObj);

  /**
   * This method achieves what the other version of alter does, but it is only
   * used during simulations. The tokens are game-specific Strings that supply
   * all the same info as was supplied by the live game. (See the gameLog, which
   * is where we anticipate such data is stored.) It is provided here with a
   * dummy implementation, but if you want to run simulations you must provide a
   * useful implementation that can interpret the gameLog token format.
   */
  public boolean interpret(long tstamp, ClientLiaison client, String[] tokens) {

    //Node node = client.eigenNode;
    int value = Integer.parseInt(tokens[3]);
    try {
      client.alter(value);
    } catch (RemoteException e) {
      e.printStackTrace();
    }
    //node.color=value;
    
    /*
    if (value < 1 || value >= GraphPanel.swatches.length) {
      System.err.println(tstamp + " client " + client + " attempted setting to wonky value " + value);
      return false;
    }
    Node node = client.eigenNode;
    // System.err.println(" event #"+ nEvents+": node "+ node +" changing to
    // "+value);
    // System.err.println(" (game pause "+gamePaused+", complete "+gameCompleted
    // +")");
    synchronized (nodeStates) { // only this section needs to be threadsafe
      if (clock.suspended() || gameCompleted)
        return false;
      nEvents++;
      gameLog.println(tstamp + " " + client.clientID + " " + node + " " + value);
      // System.err.println(" node #"+ node +" changed to "+value);
      // node.color= value; Does note update the right graph
      grpk.graph.getNode(node.getIndex()).color = value;
      nodeStates[node.getIndex()].colorInt = value; // known to be legitimate
                                                    // value
      currentProgress = getProgress();
      if (currentProgress == perfection/* solved() *//*) {
        finished("game solved", 0);
        gameValue = "solved";
        gameCompleted = true;
        System.out.println("game solved");
      }
    }*/
    monitor.repaint();
    return true;
  }

  /**
   * Gets regularly called by the Monitor thread to update the Monitor metadata.
   * 
   * @return the number of milliseconds before the next call should be scheduled
   */
  protected int monitorUpdate(Monitor mon) {
    mon.timeDisplay.setText(String.format(" %d ", clock.getElapsedTime() / 1000));
    return 1000;
  }

  enum MState {
    loadable, started, stopped
  };

  protected class Monitor extends JFrame implements ActionListener {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private Container basePanel;
    private JPanel ctlPanel;
    private JButton opr8, endG;
    private MState mstate;
    public JLabel timeDisplay, welfareDisplay;

    /**
     * The GUI that the supervisor gets to view and control while the game is
     * being played.
     * 
     * @param gPanel
     *          the view of the game
     * @param locn
     *          a suggested screen pixel location for the panel; if null, a
     *          default will be used.
     */
    public Monitor(JPanel gPanel, Point locn) {
      super();
      System.err.println("constructing Monitor");
      setTitle("Monitor for " + BNSGameController.this.getTitle());
      setSize(950, 950);
      basePanel = getContentPane();
      ctlPanel = new JPanel();
      timeDisplay = new JLabel("time");
      ctlPanel.add(timeDisplay);
      opr8 = new JButton("load");
      ctlPanel.add(opr8);
      opr8.addActionListener(this);
      ctlPanel.add(new JLabel(getSource().getName().toString()));
      endG = new JButton("end");
      ctlPanel.add(endG);
      endG.addActionListener(this);
      welfareDisplay = new JLabel("status");
      ctlPanel.add(welfareDisplay);
      basePanel.add(ctlPanel, BorderLayout.NORTH);
      basePanel.add(gPanel, BorderLayout.CENTER);
      validate();
      if (locn != null)
        setLocation(locn);
      setLocation(new Point(900, 200));
      setVisible(true); // must come BEFORE init, so that getSize() works
      repaint();
      mstate = MState.loadable;

      Thread monitorThread = new Thread(new Runnable() {
        public synchronized void run() {
          // String expName= System.getProperty("GAME"); // look for automatic
          // run
          // if (expName!=null) {
          // Thread.yield(); loadGame(); opr8.setEnabled(false); proceed(); }
          while (!gameCompleted) {
            // System.err.println("monitorThread: "+clock.getElapsedTime());
            int delay = monitorUpdate(Monitor.this); // pass this; avoid race
                                                      // condition
            try {
              wait(delay);
            } catch (InterruptedException ie) {
              System.err.println("monitorThread interrupted; WHY?");
            }
          }
          monitorUpdate(Monitor.this); // final update of clock
          // if (expName!=null) { suspend(); System.err.println("automatic
          // endG"); endGame(); }
          System.err.println("monitorThread.stopping " + clock.getElapsedTime());
        }
      });
      monitorThread.start();
      System.err.println("monitorThread.start()ed");
    }

    public void actionPerformed(ActionEvent e) {
      Object src = e.getSource();
      if (src == opr8) {
        System.err.println("Monitor opr8");
        switch (mstate) {
        case loadable:
          if (loadGame()) {
            suspend();
            mstate = MState.stopped;
            opr8.setText("start");
            endG.setEnabled(true);
            gameValue = "not started";
          }
          break;
        case started:
          if (gameCompleted) {
            opr8.setEnabled(false);
            endG.requestFocus();
          } else
            suspend();
          opr8.setText("start");
          endG.setEnabled(true);
          mstate = MState.stopped;
          break;
        case stopped:
          proceed();
          mstate = MState.started;
          opr8.setText("stop");
          endG.setEnabled(false);
          break;
        }
      }
      if (src == endG) {
        suspend();
        System.err.println("Monitor endG");
        endGame();
      }
    }

    public void shutdown() {
      gameCompleted = true;
      setVisible(false);
      dispose();
    }

  } // EOClass Monitor

} // EOC BNSGameController
